package cn.lingyangwl.agile.misc.short_link.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.lingyangwl.agile.common.core.exception.enums.*;
import cn.lingyangwl.agile.common.security.utils.*;
import cn.lingyangwl.agile.infra.*;
import cn.lingyangwl.agile.infra.module.group.model.rqrs.*;
import cn.lingyangwl.agile.misc.short_link.assembly.*;
import cn.lingyangwl.agile.misc.short_link.constants.*;
import cn.lingyangwl.agile.misc.short_link.manager.*;
import cn.lingyangwl.agile.misc.short_link.mapper.*;
import cn.lingyangwl.agile.misc.short_link.model.entity.*;
import cn.lingyangwl.agile.misc.short_link.model.rqrs.*;
import cn.lingyangwl.agile.misc.short_link.service.*;
import cn.lingyangwl.agile.misc.short_link.utils.*;
import cn.lingyangwl.agile.model.*;
import cn.lingyangwl.agile.model.enums.*;
import cn.lingyangwl.agile.model.module.auth.*;
import cn.lingyangwl.agile.tenant.core.utils.*;
import cn.lingyangwl.framework.lock.constant.*;
import cn.lingyangwl.framework.lock.manager.*;
import cn.lingyangwl.framework.lock.model.*;
import cn.lingyangwl.framework.mybatis.plus.utils.*;
import cn.lingyangwl.framework.tool.core.*;
import cn.lingyangwl.framework.tool.core.exception.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 短链
 *
 * @author shenguangyang
 */
@Slf4j
@Service
public class ShortLinkServiceImpl extends ServiceImpl<SlShortLinkMapper, ShortLink> implements ShortLinkService {

    @Resource
    private ShortLinkAssembly shortLinkAssembly;
    @Resource
    private SlShortLinkManager shortLinkManager;
    @Resource
    private SlLinkDomainManager linkDomainManager;

    @Resource(name = LockBeanName.REDISSON_SERVICE)
    private ILockManager lockManager;

    @Resource
    private InfraApi infraApi;

    /**
     * 分页查询 短链
     *
     * @return 返回分页数据
     */
    @Override
    public IPage<ShortLinkDto> pageLink(ShortLinkPageReq req) {
        req.setUserId(SecurityUtils.getUserId());
        LambdaQueryWrapper<ShortLink> queryWrapper = ShortLink.lqw()
                .eq(ShortLink::getUserId, req.getUserId())
                .eq(ShortLink::getDeleted, BoolEnum.FALSE.getCode())
                .eq(StringUtils.isNotEmpty(req.getStatus()), ShortLink::getStatus, req.getStatus())
                .eq(Objects.nonNull(req.getGroupId()), ShortLink::getGroupId, req.getGroupId())
                .eq(StringUtils.isNotEmpty(req.getTitle()), ShortLink::getTitle, req.getTitle())
                .orderByDesc(BasePlusPO::getCreatedAt);
        IPage<ShortLink> pageData = this.page(PageUtil.to(req), queryWrapper);

        List<Long> groupIds = pageData.getRecords().stream().map(ShortLink::getGroupId).distinct().collect(Collectors.toList());
        Set<Long> domainIds = pageData.getRecords().stream().map(ShortLink::getDomainId).collect(Collectors.toSet());
        domainIds.add(-1L);
        groupIds.add(-1L);

        Map<Long, LinkDomain> domainMap = linkDomainManager.listByIds(domainIds).stream()
                .collect(Collectors.toMap(LinkDomain::getId, Function.identity()));

        DataGroupQuery groupQuery = new DataGroupQuery().setIds(groupIds).setBizType(DataGroupBizTypeEnum.SHORT_LINK.getType());
        Map<Long, DataGroupDto> groupMap = infraApi.listDataGroup(groupQuery).stream()
                .collect(Collectors.toMap(DataGroupDto::getId, Function.identity()));

        // 获取过期的短链并更新
        List<Long> expiredIdList = pageData.getRecords().stream()
                .filter(e -> Objects.nonNull(e.getExpireTime()) && new Date().after(e.getExpireTime()))
                .peek(e -> e.setStatus(ShortLink.Status.EXPIRED)).map(ShortLink::getId)
                .collect(Collectors.toList());
        updateToExpired(expiredIdList);
        return PageUtil.to(pageData, (e) -> {
            ShortLinkDto resp = shortLinkAssembly.toDto(e);
            resp.setOriginalUrl(ShortLinkUtils.removeUrlPrefix(e.getOriginalUrl()));
            resp.setGroupName(Optional.ofNullable(groupMap.get(e.getGroupId())).map(DataGroupDto::getName).orElse(null));
            resp.setGroupId(groupMap.containsKey(e.getGroupId()) ? e.getGroupId() : null);
            String domain = Optional.ofNullable(domainMap.get(e.getDomainId())).map(LinkDomain::getDomain).orElse("");
            resp.setShortUrl(UrlUtils.removeRepeatSlashOfUrl(domain + "/" + e.getCode()));
            return resp;
        });
    }

    public void updateToExpired(List<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return;
        }
        LambdaUpdateWrapper<ShortLink> lqw = ShortLink.luw().in(ShortLink::getId, ids)
                .set(ShortLink::getStatus, ShortLink.Status.EXPIRED);
        this.update(lqw);
    }

    /**
     * 根据主键查询 商家短链
     *
     * @param id 主键
     * @return 返回记录，没有返回null
     */
    @Override
    public ShortLink getLinkById(Long id, String code) {
        LambdaQueryWrapper<ShortLink> queryWrapper = ShortLink.lqw()
                .eq(ShortLink::getId, id)
                .eq(ShortLink::getCode, code);
        ShortLink shortLink = this.getOne(queryWrapper);

        DataGroupDto dataGroup = infraApi.getDataGroup(shortLink.getGroupId());
        if (dataGroup == null) {
            shortLink.setGroupId(null);
        }
        return shortLink;
    }

    @Override
    public ShortLink getLinkByCode(String code) {
        return TenantUtils.executeIgnore(() -> this.getOne(ShortLink.lqw().eq(ShortLink::getCode, code)));
    }

    @Override
    public boolean saveLink(ShortLinkSaveReq req) {
        Long userId = SecurityUtils.getUserId();
        req.setUserId(userId);
        req.setOriginalUrl(ShortLinkUtils.addUrlPrefix(req.getOriginalUrl()));

        // 校验域名是否合法
        LinkDomain linkDomain = checkDomain(req.getDomainId());
        // 校验组是否合法
        DataGroupDto groupDto = infraApi.getDataGroup(req.getGroupId());
        if (Objects.isNull(groupDto) || groupDto.getDeleted()) {
            throw new BizException("组不存在或已被删除");
        }

        for (int i = 0; i < 5; i++) {
            // 生成摘要
            String sign = MD5Utils.md5Lower(req.getOriginalUrl());
            // 生成短链码
            String shortLinkCode = shortLinkManager.createShortLinkCode(sign);

            ILock lock = lockManager.getLock(String.format(CacheKeyCons.SHORT_LINK_ADD, shortLinkCode));
            lock.tryLock(2, TimeUnit.SECONDS);
            try {
                if (doSave(req, linkDomain, sign, shortLinkCode)) {
                    return true;
                }
                req.setOriginalUrl(ShortLinkUtils.addUrlPrefixVersion(req.getOriginalUrl()));
                TimeUnit.MILLISECONDS.sleep(100);
                log.error("短链码重复, code: {}, tryCount: {}, ",  shortLinkCode, i);
            } catch (Exception e) {
                log.error("error: {}", e.getMessage());
                throw new BizException("生成短链失败");
            } finally {
                lock.unlock();
            }
        }
        return true;
    }

    private boolean doSave(ShortLinkSaveReq req, LinkDomain linkDomain, String sign, String shortLinkCode) {
        ShortLink entity = shortLinkAssembly.toPo(req);
        entity.setId(null);
        entity.setUserId(req.getUserId());
        entity.setCode(shortLinkCode);
        entity.setSign(sign);
        entity.setDomain(linkDomain.getDomain());
        entity.setGroupId(req.getGroupId());
        entity.setStatus(ShortLink.Status.ACTION);
        return shortLinkManager.save(entity);
    }

    private LinkDomain checkDomain(Long domainId) {
        LinkDomain entity = linkDomainManager.getById(domainId);
        Assert.notNull(entity, "域名不合法");
        return entity;
    }

    @Override
    public boolean updateLink(ShortLinkUpdateReq req) {
        ShortLink linkGroupManager = this.getById(req.getId());
        if (Objects.isNull(linkGroupManager)) {
            throw new BizException(GlobalErrorEnum.OPERATION_FAIL_SELETE);
        }

        LoginUser loginUser = SecurityUtils.getLoginUser();
        ShortLink entity = shortLinkAssembly.toPo(req);
        entity.setUserId(loginUser.getUserId());

        // 如果过期了, 设置状态为过期状态
        // 如果没有过期且旧状态为过期, 则设置状态为启用
        boolean expired = Objects.nonNull(entity.getExpireTime()) && new Date().after(entity.getExpireTime());
        if (expired) {
            entity.setStatus(ShortLink.Status.EXPIRED);
        } else if (ShortLink.Status.EXPIRED.equals(linkGroupManager.getStatus())) {
            entity.setStatus(ShortLink.Status.ACTION);
        }

        // 校验域名是否合法
        checkDomain(req.getDomainId());

        // 校验组是否合法
        DataGroupDto groupDto = infraApi.getDataGroup(req.getGroupId());
        if (Objects.isNull(groupDto) || groupDto.getDeleted()) {
            throw new BizException("组不存在或已被删除");
        }
        shortLinkManager.updateById(entity);
        return true;
    }

    /**
     * 通过ids删除 商家短链

     * @return 返回影响行数
     */
    @Override
    public boolean deleteLinkByIds(ShortLinkDeleteReq rq) {
        rq.getIds().add(-1L);
        LambdaQueryWrapper<ShortLink> lqw = ShortLink.lqw().in(ShortLink::getId, rq.getIds());
        return this.remove(lqw);
    }

    @Override
    public boolean batchDeleteGroup(List<Long> ids) {
        DataGroupQuery groupReq = new DataGroupQuery();
        DataGroupBizTypeEnum bizTypeEnum = DataGroupBizTypeEnum.SHORT_LINK;
        groupReq.setBizType(bizTypeEnum.getType()).setIds(ids);
        List<DataGroupDto> groupList = infraApi.listDataGroup(groupReq);
        List<Long> groupIds = groupList.stream().map(DataGroupDto::getId).distinct().collect(Collectors.toList());
        if (this.count(ShortLink.lqw().in(ShortLink::getGroupId, groupIds)) > 0) {
            throw new BizException("所删除的组已被短链引用, 请删除全部短链后再删除组");
        }

        DataGroupDeleteReq deleteReq = new DataGroupDeleteReq();
        deleteReq.setBizType(bizTypeEnum.getType()).setIds(ids);
        infraApi.batchDeleteDataGroup(deleteReq);
        return true;
    }

    @Override
    public void addAccountCount(Long linkId, String code) {
        if (ObjectUtils.anyNull(linkId, code)) {
            log.warn("linkId and code any is null, add access count fail");
            return;
        }
        this.baseMapper.addAccessCount(linkId, code, 1L);
    }
}
